更多安全资讯和分析文章请关注启明星辰ADLab微信公众号及官方网站(adlab.venustech.com.cn)
数月前,国外安全组织ZDI研究人员披露了一个Linux内核本地权限提升漏洞,该漏洞出现在流量控制子系统包分类器的cls_route过滤器中,当旧过滤器句柄为0时,在释放之前内核不会从哈希表中将其删除,其漏洞编号为CVE-2022-2588,而且还提出了一种新的漏洞利用方法,命名为DirtyCred,该方法可绕过广泛采用的内核保护和漏洞利用缓解措施,从而实现权限提升。Rtnetlink是所有内核网络子系统使用的网络连接总线,包括网络接口、路由、fdb和邻居。一些内核网络子系统也在通用netlink总线上提供服务。Linux内核网络子系统使用消息类型和系列向Rtnetlink内核注册处理程序。Rtnetlink允许读取和更改内核的路由表。它在内核中用于在各种子系统之间进行通信,也用于与用户空间程序进行通信。网络路由、IP地址、链接参数、邻居设置、排队规则、流量类别和数据包分类器都可以通过NETLINK_ROUTE套接字进行控制。Rtnetlink由以下消息类型组成(除了标准的netlink消息):RTM_NEWLINK、RTM_DELLINK、RTM_GETLINK创建、删除或获取有关特定网络接口的信息。
RTM_NEWADDR、RTM_DELADDR、RTM_GETADDR添加、删除或接收有关与接口关联的IP地址的信息。
RTM_NEWROUTE、RTM_DELROUTE、RTM_GETROUTE创建、删除或接收有关网络路由的信息。
RTM_NEWNEIGH、RTM_DELNEIGH、RTM_GETNEIGH添加、删除或接收有关邻居表条目的信息(例如,ARP条目)。
RTM_NEWRULE、RTM_DELRULE、RTM_GETRULE添加、删除或检索路由规则。
RTM_NEWQDISC、RTM_DELQDISC、RTM_GETQDISC添加、删除或获取排队规则。
RTM_NEWTCLASS、RTM_DELTCLASS、RTM_GETTCLASS添加、删除或获取流量类别。
RTM_NEWTFILTER, RTM_DELTFILTER, RTM_GETTFILTER添加、删除或接收有关流量过滤器的信息。
当内核启动加载,在初始化netlink协议实现时,会调用rtnetlink_init()函数初始化路由netlink socket接口,该函数实现如下:根据代码可知,通过rtnl_register()函数将不同的消息类型和对应操作进行绑定,该函数签名为void rtnl_register(int protocol, int msgtype,rtnl_doit_func doit,
rtnl_dumpit_func dumpit,unsigned int flags),有的消息类型只有动作函数doit,有的消息类型只有dump函数dompit,有的消息类型两者皆有。有的消息类型例如RTM_NEWTFILTER,添加一个流量过滤器,则是在tc_filter_init()函数中初始化,该函数实现如下:当用户层通过NETLINK_ROUTE套接字发送RTM_NEWTFILTER消息用于创建一个流量过滤器时,内核将调用rtnetlink_rcv_msg()函数来处理rtnetlink消息,该函数关键实现如下:从消息中获取family和type,然后调用rtnl_get_link()函数根据family和type获取link,行5246,调用link->doit回调函数,这里的doit回调函数即为tc_new_tfilter()函数。该函数会进一步解析rtnetlink消息数据包,判断并创建哪种类型的过滤器,具体实现如下:获取指定的过滤器名字,然后会获取指定协议的过滤器tp。如下代码所示:如果没有,便根据name创建一个新的tp。如下代码所示:
判断tca[TCA_KIND]不为空和nlmsg_flags为NLM_F_CREATE,然后调用tcf_proto_create()函数创建,该函数实现如下:行258,分配一个tp;行262,调用tcf_proto_lookup_ops()函数根据kind获取对应的ops。这里以route为例,将获取到cls_route4_ops,如下代码所示:然后初始化tp,行274,调用route4_init()函数初始化一个route4_head,用于存放过滤器对应的哈希值,该函数实现如下:tcf_proto创建完成后,将其插入chain中。如下代码所示:接下来调用对应的get函数,根据tcm_handle获取过滤器。这里将调用route4_get()函数获取,该函数实现如下:根据handle从route4_head链表中获取对应的route4_filter。如果为空,便调用change函数进行创建。如下代码所示:这里将调用ruote4_change()函数进行创建,该函数具体分析见下文。创建成功后,添加一个新的route4过滤器的整个流程便完成了。漏洞代码出现在route4_change()函数中,该函数实现如下:行483,首先进一步解析消息数据包;行488,拿出传入的route4_filter,然后判断是否已创建,如果创建过,再判断handle是否一致。由于第一次创建,这里fold为空。接下来进行创建并初始化,如下代码所示:行493,调用kzalloc()函数分配route4_filter,该结构体大小为144字节。行497,调用tcf_exts_init()函数进行初始化,该函数实现如下:如果内核开启了CONFIG_NET_CLS_ACT,便调用kcalloc()函数分配exts->actions,分配大小为256字节。初始化完成返回到route4_change()函数中,行501,如果fold不为空,便将fold的数据域赋值给f。行512,然后调用route4_set_parms()函数设置其他参数。行517到行527,将新创建的route4_filter对应的哈希值放到route4_head中。如下代码所示:行529到行543,该段代码是将旧过滤器的哈希值从route4_head中移除。如下代码所示:由于是第一次创建,fold为空,因此不进入。但是行529代码是有问题的,这里不仅判断fold是否为空,同时还判断fold->handle是否为空。那如果第一次创建一个handle为0的过滤器,第二次创建新过滤器时,fold不为空,但是fold->handle为0,因此并不会将handle为0的旧过滤器的哈希值从route4_head中移除,保留了对其的索引。接下来开始释放旧过滤器内存,如下代码所示:第一次创建过滤器时,fold为空,不进入。当第二次创建新过滤器时,fold不为空,行550,调用tcf_queue_work()函数对handle为0的旧过滤器进行释放操作,回调函数为route4_delete_filter_work()函数,该函数实现如下:行266,最后调用__route4_delete_filter()函数分别释放f->exts和f。当内核调用route4_delete()函数进行释放所有过滤器时,行344,会调用route4_delete_filter_work()函数进行释放,该函数实现如下:由于handle为0的旧过滤器对应的哈希值依然在route4_head中,因此会对两个对象进行二次释放,分别是route4_filer及对应的exts。该漏洞修复补丁如下代码所示:将判断条件改成fold是否为空,fold不为空便将其从哈希表中删除。研究人员提出了一种新的漏洞利用方法,命名为DirtyCred,该利用方法将非特权cred与特权cred进行交换以提升权限,且不需要覆盖内核堆栈上的任何关键数据字段,而是滥用堆内存重写机制来获得特权。该利用技术无需泄露内核地址绕过KASLR,且通用性较强,可跨不同的内核和架构,能绕过广泛采用的内核保护和漏洞利用缓解措施。DirtyCred分为三个步骤,过程如下图所示:首先,DirtyCred打开一个可写文件“/tmp/x”,它将在内核中分配一个可写file对象。通过触发漏洞,源指针会引用相应缓存中的file对象。然后,DirtyCred尝试将内容写入打开的文件“/tmp/x”中。在实际写入内容之前,内核会检查当前文件是否有写权限,该位置是否可写等。通过检查后,DirtyCred会继续这个实际的文件写入操作,进入第二步。在此步骤中,DirtyCred触发fs_context对象的释放点以解除分配file对象,这就使file对象成为一个被释放的内存点。第三步,DirtyCred打开了一个只读文件“/etc/passwd”,这触发了内核为“/etc/passwd”分配file对象。如上图所示,新分配的file对象接管了被释放的位置。完成此设置后,DirtyCred将释放其暂停的写入操作,而内核将执行实际的内容写入。由于file对象已经被调换,搁置的内容将被重定向到只读文件“/etc/passwd”中。假设写入“/etc/passwd”的内容是“hacker:x:0:0:root:/:/bin/sh”,轻易地注入一个特权账户,从而实现权限提升。(1)file slab碎片整理
首先创建10000个“/etc/passwd”的file对象,进行file slab碎片整理,为堆喷做准备。(2)缓存跨越(cross-cache)
当为特定类型的对象创建专用的内存缓存时,该缓存中将只存在该类型的对象,从而防止不同类型的对象相邻。但是,不同slab cache的slab page可以相邻。当slab A与slab B相邻时,slab A末尾的对象与slab B开头的另一个对象相邻。因此,攻击者有可能放置带有任何类型受害者对象的slab页在易受攻击对象的slab page之后,溢出受害者对象来执行攻击。当一个slab page被释放给伙伴系统时,它会在稍后的某个时候被重用,因为内存页面应该被内核回收。cross-cache攻击的技术是释放slab
page中的所有内存槽,强制释放slab page。然后再喷射另一种类型的对象分配新的slab page,回收释放的slab page。如果攻击成功,被释放对象的内存将被另一种类型的对象占用。由于file对象在专属缓存中分配,而漏洞中route4_filter和exts在通用缓存中分配,正常情况下,两个缓存是隔离的,无法进行常规的对象占坑。作者采用了缓存跨越攻击解决了这个问题。前文讲到route4_filter对象大小为144,在kmalloc-192中分配,exts对象大小为256,在kmalloc-256中分配。file对象大小为256(默认配置情况下),在专属内存中分配。先通过创建basic过滤器在kmalloc-256 slab中进行内存布局。然后创建route4过滤器,将会创建一个route4_filter漏洞对象和exts对象。最后再进行一部分basic过滤器创建,完成内存布局。关键对象内存分布如下图所示:然后第一次触发漏洞,释放route4_filter对象和exts对象。然后再全部释放basic->exts对象,让伙伴系统回收kmalloc-256 slab page。由于内核默认开启slab double free缓解机制,所以这里要乱序释放多个basic->exts。(3)堆喷占坑
大量打开一个普通文件data2,进行file对象分配占坑route4->exts。缓存跨越攻击成功后,然后再次触发漏洞,第二次释放route4_filter,跟着释放route4->exts,将会把file对象非法释放掉。(4)寻找file对象空洞
二次释放后,出现了一个file对象空洞,需要找到它。所以此时再次大量打开另一个普通文件uaf,进行file对象堆喷,将会占据这个file对象空洞。然后通过kcmp()系统调用检查 pid1 和 pid2 标识的两个进程是否共享文件描述符定位fds[j]。(5)延长时间窗口和写恶意数据
找到目标file对象的文件描述符fds[j]后,启动两个线程,slow_write线程向uaf文件中写2G数据,用于延长时间窗口。然后write_cmd线程通过fds[j]文件描述符写恶意数据。(6)再次释放并堆喷
最后进行close操作,释放file对象,随即大量打开“/etc/passwd”文件进行file对象堆喷。堆喷成功后,顺利向“/etc/passwsd”写数据,注入一个特权账户完成权限提升。
在内核版本5.4.124中,开启CONFIG_NET_CLS_ACT和CONFIG_NET_SCH_SFQ,可成功复现漏洞。打印调试关键数据,如下图所示:
参考链接:
[1] https://grsecurity.net/how_autoslab_changes_the_memory_unsafety_game[2] https://www.openwall.com/lists/oss-security/2022/08/09/6[3] https://www.man7.org/linux/man-pages/man7/rtnetlink.7.html[4] https://legacy.netdevconf.info/0.1/papers/Rtnetlink-dump-filtering-in-the-kernel.pdf[5] https://github.com/Markakd/DirtyCred[6] https://github.com/Markakd/CVE-2022-2588
[7]https://zplin.me/papers/DirtyCred.pdf
启明星辰积极防御实验室(ADLab)
ADLab成立于1999年,是中国安全行业最早成立的攻防技术研究实验室之一,微软MAPP计划核心成员,“黑雀攻击”概念首推者。截止目前,ADLab已通过CVE累计发布安全漏洞近1100个,通过 CNVD/CNNVD累计发布安全漏洞2000余个,持续保持国际网络安全领域一流水准。实验室研究方向涵盖操作系统与应用系统安全研究、移动智能终端安全研究、物联网智能设备安全研究、Web安全研究、工控系统安全研究、云安全研究。研究成果应用于产品核心技术研究、国家重点科技项目攻关、专业安全服务等。